Customer Churn em Operadora de Telecom

1. Introdução


O churn trata da perda de clientes sofrida por uma empresa para concorrência, ou seja, é uma medida da infidelidade dos clientes. Vários setores da economia tem que administrar o índice de churn de seus clientes. Bancos e administradoras de cartão de crédito são exemplos bem conhecidos, bem como empresas de telcomunicações.

Há três tipos de churn: involuntário, voluntário e inevitável.

  • Involuntário - quando o usuário deixa de pagar pelo serviço e tem seu fornecimento cancelado. Os motivos pelos quais o cliente deixa de pagar podem ser os mais diversos, como desemprego, falta de capital suficiente para se manter entre outros.
  • Voluntário - quando o cliente decide mudar de fornecedor, seduzido por campanhas de marketing e/ou promoções.

  • Inevitável - quando o usuário vem a falecer ou muda-se para uma localidade não atendida pelo fornecedor.

É provado que o custo para se manter um cliente é muito menor que o de se conquistar um novo cliente. Para se evitar o churn, empregam-se ferramentas de mineração de dados e estatística multivariada. Estas ferramentas permitem que se analise o banco de dados com informações do perfil histórico de cada usuário e que se determine quais clientes são leais, quais são propensos ao churn e quais são realmente de alto valor para a empresa.

Fonte: Andrade (2007)

2. Objetivos específicos

  • Analisar os dados fornecidos para entender o perfil dos clientes
  • Utilizar um modelo de machine learning para prever se um cliente pode ou não cancelar seu plano, e qual a probabilidade disso ocorrer.

3. Base de dados

3.1 Conteúdo da base de dados

Cada linha representa um cliente, cada coluna contém os atributos do cliente descritos na coluna Metadados.

O conjunto de dados inclui informações sobre:

  • Clientes que saíram no último mês - a coluna é chamada de Churn Services que cada cliente assinou - telefone, múltiplas linhas, internet, segurança online, backup online, proteção de dispositivo, suporte técnico e streaming de TV e filmes;
  • Informações da conta do cliente - há quanto tempo ele é cliente, contrato, forma de pagamento, faturamento sem papel, cobranças mensais e cobranças totais Informações demográficas sobre clientes - sexo, faixa etária e se eles têm parceiros e dependentes

3.2 Descrição das colunas

Coluna Descrição
Customer ID Código de identificação do cliente
gender Indica se o cliente é masculino ou feminino
SeniorCitizen Indica se o cliente é idoso (> 65 anos) ou não
Partner Indica se o cliente tem um parceiro ou não
Dependents Indica se o cliente tem dependentes ou não
tenure Número de meses que o cliente permaneceu na empresa
PhoneService Indica se o cliente tem um serviço de telefone ou não
MultipleLines Indica se o cliente possui múltiplas linhas ou não
InternetService Provedor de serviços de Internet do cliente
OnlineSecurity Indica se o cliente tem serviço de segurança online ou não
Online Backup Indica se o cliente possui um serviço de backup adicional oferencido pela empresa
DeviceProtection Indica se o cliente se inscreveu em um plano de proteção de dispositivos adicional
TechSupport Indica se o cliente se inscreveu em um plano de suporte técnico adicional
StreamingTV Indica se o cliente usa um serviço de streaming de programação de televisão pela internet provido por terceiros (A empresa não cobra taxa adicional por esse serviço)
StreamingMovies Indica se o cliente usa um serviço de streaming de filmes pela internet provido por terceiros (A empresa não cobra taxa adicional por esse serviço)
Contract Indica o atual tipo de contrato do cliente
PaperlessBilling Indica se o cliente escolheu o faturamento sem papel
PaymentMethod Indica como o cliente paga a fatura
MonthlyCharges Indica a cobrança mensal total atual do cliente por todos os serviços da empresa.
TotalCharges Indica as cobranças totais do cliente, calculadas até o final do trimestre especificado
Churn Indica se o cliente deixou ou não a empresa
In [1]:
# importação da biblioteca pandas para manipulação do dataframe
import pandas as pd
In [2]:
# obtenção dos dados

fonte = r'C:\Users\Andre\Desktop\Telco\WA_Fn-UseC_-Telco-Customer-Churn.csv'

O download do arquivo de dados pode ser feito nesse link.

In [3]:
dados = pd.read_csv(fonte)
In [4]:
# visualização das primeiras 5 linhas dos dados 
dados.head()
Out[4]:
customerID gender SeniorCitizen Partner Dependents tenure PhoneService MultipleLines InternetService OnlineSecurity ... DeviceProtection TechSupport StreamingTV StreamingMovies Contract PaperlessBilling PaymentMethod MonthlyCharges TotalCharges Churn
0 7590-VHVEG Female 0 Yes No 1 No No phone service DSL No ... No No No No Month-to-month Yes Electronic check 29.85 29.85 No
1 5575-GNVDE Male 0 No No 34 Yes No DSL Yes ... Yes No No No One year No Mailed check 56.95 1889.5 No
2 3668-QPYBK Male 0 No No 2 Yes No DSL Yes ... No No No No Month-to-month Yes Mailed check 53.85 108.15 Yes
3 7795-CFOCW Male 0 No No 45 No No phone service DSL Yes ... Yes Yes No No One year No Bank transfer (automatic) 42.30 1840.75 No
4 9237-HQITU Female 0 No No 2 Yes No Fiber optic No ... No No No No Month-to-month Yes Electronic check 70.70 151.65 Yes

5 rows × 21 columns

4. Análise exploratória dos dados

In [5]:
# importação das bibliotecas

import numpy as np
from matplotlib import pyplot as plt
import seaborn as sns
import plotly.express as px
import scikitplot as skplt
import sklearn.metrics as metrics
import warnings
warnings.filterwarnings('ignore')
In [6]:
# definindo algumas funções para plot de gráficos

# gráfico de barras
def grafbarra(x=1, y=2, dados=None,
              rotulox=None, rotuloy=None,
              xticklabels=None, alturay=1):
  
  g = sns.catplot(x=x, y=y,
                data=dados, saturation=.6,
                kind="bar", ci=None, aspect=1, height=6)
  (g.set_axis_labels(rotulox, rotuloy)
    .set_xticklabels(xticklabels)
    .set_titles("{col_name} {col_var}")
    .set(ylim=(0, alturay))
    .despine(left=True)) 

#histogramas
def histograma(dados=None, x=None, nbins=20):
  fig = px.histogram(dados, x=x, nbins=nbins)
  fig.show()


# boxplot
def boxplot(dados=None, x=None, y=None, title_y=None, title_x=None, titulograf=None):
  fig = px.box(dados, x=x, y=y)

  # Update yaxis properties
  fig.update_yaxes(title_text=title_y , row=1, col=1)
  # Update xaxis properties
  fig.update_xaxes(title_text=title_x, row=1, col=1)

  # Update size and title
  fig.update_layout(autosize=True, width=750, height=600,
      title_font=dict(size=25, family='Courier'),
      title=titulograf,
  )

  fig.show()

# gráfico de correlação
def plot_corr(dados):
  plt.figure(figsize=(15,15))
  corr = dados.corr()
  sns.heatmap(corr, annot=True)
In [7]:
dados.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 21 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   customerID        7043 non-null   object 
 1   gender            7043 non-null   object 
 2   SeniorCitizen     7043 non-null   int64  
 3   Partner           7043 non-null   object 
 4   Dependents        7043 non-null   object 
 5   tenure            7043 non-null   int64  
 6   PhoneService      7043 non-null   object 
 7   MultipleLines     7043 non-null   object 
 8   InternetService   7043 non-null   object 
 9   OnlineSecurity    7043 non-null   object 
 10  OnlineBackup      7043 non-null   object 
 11  DeviceProtection  7043 non-null   object 
 12  TechSupport       7043 non-null   object 
 13  StreamingTV       7043 non-null   object 
 14  StreamingMovies   7043 non-null   object 
 15  Contract          7043 non-null   object 
 16  PaperlessBilling  7043 non-null   object 
 17  PaymentMethod     7043 non-null   object 
 18  MonthlyCharges    7043 non-null   float64
 19  TotalCharges      7043 non-null   object 
 20  Churn             7043 non-null   object 
dtypes: float64(1), int64(2), object(18)
memory usage: 1.1+ MB
In [8]:
# Verificando se há registros duplicados no dadaset.

a = dados.duplicated().sum()
print(f'Há um total de {a} dados duplicados.')
Há um total de 0 dados duplicados.
In [9]:
# Verificando se há registros nulos

dados.isna().sum()
Out[9]:
customerID          0
gender              0
SeniorCitizen       0
Partner             0
Dependents          0
tenure              0
PhoneService        0
MultipleLines       0
InternetService     0
OnlineSecurity      0
OnlineBackup        0
DeviceProtection    0
TechSupport         0
StreamingTV         0
StreamingMovies     0
Contract            0
PaperlessBilling    0
PaymentMethod       0
MonthlyCharges      0
TotalCharges        0
Churn               0
dtype: int64

4.1 Levantamento de hipóteses

  1. Há correlação entre a quantidade de serviços contratados por cliente e sua permanência com a empresa?
  2. Existe predominância do gênero dos clientes e a desvinculação com a empresa?
  3. Qual a relação entre o valor do plano pago por mês e os clientes que deixaram a empresa?
  4. Qual a proporção de clientes que deixaram a empresa que possuiam linha telefônica?

Primeiramente, realizaremos mudança do tipo de variável categórica para númerica em algumas colunas para melhor análise.

In [10]:
# visualizando a primeira linha do dataframe

dados.head(1)
Out[10]:
customerID gender SeniorCitizen Partner Dependents tenure PhoneService MultipleLines InternetService OnlineSecurity ... DeviceProtection TechSupport StreamingTV StreamingMovies Contract PaperlessBilling PaymentMethod MonthlyCharges TotalCharges Churn
0 7590-VHVEG Female 0 Yes No 1 No No phone service DSL No ... No No No No Month-to-month Yes Electronic check 29.85 29.85 No

1 rows × 21 columns

In [11]:
# listando as colunas do dataframe

dados.columns.values
Out[11]:
array(['customerID', 'gender', 'SeniorCitizen', 'Partner', 'Dependents',
       'tenure', 'PhoneService', 'MultipleLines', 'InternetService',
       'OnlineSecurity', 'OnlineBackup', 'DeviceProtection',
       'TechSupport', 'StreamingTV', 'StreamingMovies', 'Contract',
       'PaperlessBilling', 'PaymentMethod', 'MonthlyCharges',
       'TotalCharges', 'Churn'], dtype=object)
In [12]:
# utilizando o método replace do pandas para mudança das variáveis categóricas para númericas

dados.Partner.replace({'Yes': 1, 'No': 0}, inplace=True)
dados.Dependents.replace({'Yes': 1, 'No': 0}, inplace=True)
dados.PhoneService.replace({'Yes': 1, 'No': 0}, inplace=True)
dados.OnlineSecurity.replace({'Yes': 1, 'No': 0, 'No internet service': 0 }, inplace=True)
dados.OnlineBackup.replace({'Yes': 1, 'No': 0, 'No internet service': 0}, inplace=True)
dados.DeviceProtection.replace({'Yes': 1, 'No': 0, 'No internet service': 0}, inplace=True)
dados.TechSupport.replace({'Yes': 1, 'No': 0, 'No internet service': 0}, inplace=True)
dados.StreamingTV.replace({'Yes': 1, 'No': 0, 'No internet service': 0}, inplace=True)
dados.StreamingMovies.replace({'Yes': 1, 'No': 0, 'No internet service': 0}, inplace=True)
dados.PaperlessBilling.replace({'Yes': 1, 'No': 0}, inplace=True)
dados.gender.replace({'Female': 1, 'Male': 0}, inplace=True)
In [13]:
dados.head(1)
Out[13]:
customerID gender SeniorCitizen Partner Dependents tenure PhoneService MultipleLines InternetService OnlineSecurity ... DeviceProtection TechSupport StreamingTV StreamingMovies Contract PaperlessBilling PaymentMethod MonthlyCharges TotalCharges Churn
0 7590-VHVEG 1 0 1 0 1 0 No phone service DSL 0 ... 0 0 0 0 Month-to-month 1 Electronic check 29.85 29.85 No

1 rows × 21 columns

Na célula abaixo, criaremos uma coluna chamada 'serviços_adic' em um novo dataframe, com a soma dos serviços adicionais contradados por cada cliente. O objetivo é verificar se a contratação de serviços adicionais pelo cliente faz com que ele mantenha o contrato com a empresa.

In [14]:
conta_serv = dados.loc[:, ['OnlineSecurity', 'OnlineBackup', 'DeviceProtection', 'TechSupport', 'StreamingTV', 'StreamingMovies']]
conta_serv['serviços_adic'] = conta_serv.sum(axis=1)
conta_serv.head()
Out[14]:
OnlineSecurity OnlineBackup DeviceProtection TechSupport StreamingTV StreamingMovies serviços_adic
0 0 1 0 0 0 0 1
1 1 0 1 0 0 0 2
2 1 1 0 0 0 0 2
3 1 0 1 1 0 0 3
4 0 0 0 0 0 0 0
In [15]:
# separando as colunas de soma dos serviços_adic e Churn em um novo dataframe

churn_serv = pd.concat([conta_serv['serviços_adic'], dados['Churn']], axis=1)
churn_serv.head()
Out[15]:
serviços_adic Churn
0 1 No
1 2 No
2 2 Yes
3 3 No
4 0 Yes
In [16]:
grafbarra(x="Churn", y="serviços_adic", dados=churn_serv, rotulox='Churn', rotuloy='Serviços adicionais', alturay=2.5)

O gráfico da célula acima mostra que, os clientes com mais serviços contratados mantiveram seu plano com a empresa.

In [17]:
# definindo o nome das linhas da coluna 'gender' de 0 para homem e 1 para mulher

genero = dados['gender'].map({0:'Homem', 1:'Mulher'})


churn = dados['Churn'].map({'No': 0, 'Yes':1})
In [18]:
genero_churn = pd.concat([dados['gender'], churn], axis=1)
genero_churn.head()
Out[18]:
gender Churn
0 1 0
1 0 0
2 0 1
3 0 0
4 1 1
In [19]:
total = (genero_churn['Churn'].sum()/len(genero_churn))*100
print(f'Média aritmética dos clientes perdidos pela empresa: {round(total, 2)} %')
Média aritmética dos clientes perdidos pela empresa: 26.54 %
In [20]:
grafbarra(x='gender', y='Churn', dados=genero_churn, rotulox='Gênero', rotuloy='Churn', xticklabels=('Homem', 'Mulher'), alturay=0.3)

O gráfico acima mostra que não há grandes disparidades entre os sexos dos clientes que deixaram a empresa.

In [21]:
grafbarra(x="Churn", y="PhoneService", dados=dados, rotulox='Churn', rotuloy='Serviço telefônico contratado', alturay=1)

O gráfico acima indica que não há grande influência dos clientes que possuiam, ou não, linha telefônica, na decisão de deixar a empresa.

In [22]:
grafbarra(y='SeniorCitizen', x='Churn', dados=dados, rotuloy='SeniorCitizen', rotulox='Churn', alturay=0.3)

A maioria dos clientes maiores que 65 anos deixaram a empresa.

In [23]:
boxplot(dados, x='Churn', y='MonthlyCharges', title_x='Churn', title_y='Pagamento por mês', titulograf='Pagamento por mês x Churn')
  • Os clientes que possuem planos mais altos tendem em deixar a empresa.
In [24]:
boxplot(dados, x='Churn', y='tenure', title_x='Churn', title_y='Tempo de permanência do cliente (Meses) por mês', titulograf='Tempo de permanência x Churn')
  • Há tendência dos clientes mais novos em a deixar a empresa. A mediana do tempo de permanência desses clientes é de 10 meses.

5. Feature Engineering

In [25]:
dados.columns
Out[25]:
Index(['customerID', 'gender', 'SeniorCitizen', 'Partner', 'Dependents',
       'tenure', 'PhoneService', 'MultipleLines', 'InternetService',
       'OnlineSecurity', 'OnlineBackup', 'DeviceProtection', 'TechSupport',
       'StreamingTV', 'StreamingMovies', 'Contract', 'PaperlessBilling',
       'PaymentMethod', 'MonthlyCharges', 'TotalCharges', 'Churn'],
      dtype='object')
In [26]:
# transformando as variável categóricas restantes em variável númericas utilizando o OneHotEncoder

from sklearn.preprocessing import OneHotEncoder

ohe = OneHotEncoder(sparse=False)
dados['InternetService'] = ohe.fit_transform(dados[['InternetService']])
dados['Contract'] = ohe.fit_transform(dados[['Contract']])
dados['MultipleLines'] = ohe.fit_transform(dados[['MultipleLines']])
dados['PaymentMethod'] = ohe.fit_transform(dados[['PaymentMethod']])
In [27]:
#visualizando as primeiras 5 linhas do dataframe

dados.head()
Out[27]:
customerID gender SeniorCitizen Partner Dependents tenure PhoneService MultipleLines InternetService OnlineSecurity ... DeviceProtection TechSupport StreamingTV StreamingMovies Contract PaperlessBilling PaymentMethod MonthlyCharges TotalCharges Churn
0 7590-VHVEG 1 0 1 0 1 0 0.0 1.0 0 ... 0 0 0 0 1.0 1 0.0 29.85 29.85 No
1 5575-GNVDE 0 0 0 0 34 1 1.0 1.0 1 ... 1 0 0 0 0.0 0 0.0 56.95 1889.5 No
2 3668-QPYBK 0 0 0 0 2 1 1.0 1.0 1 ... 0 0 0 0 1.0 1 0.0 53.85 108.15 Yes
3 7795-CFOCW 0 0 0 0 45 0 0.0 1.0 1 ... 1 1 0 0 0.0 0 1.0 42.30 1840.75 No
4 9237-HQITU 1 0 0 0 2 1 1.0 0.0 0 ... 0 0 0 0 1.0 1 0.0 70.70 151.65 Yes

5 rows × 21 columns

In [28]:
# utilizando o método replace para alterar a coluna Churn de catégorica para numérica

dados.Churn.replace({'Yes': 1, 'No': 0}, inplace=True)
In [29]:
dados['TotalCharges'] = pd.to_numeric(dados.TotalCharges, errors='coerce')
In [30]:
# verificando se há dados ausentes no dataframe

dados.isnull().sum()
Out[30]:
customerID           0
gender               0
SeniorCitizen        0
Partner              0
Dependents           0
tenure               0
PhoneService         0
MultipleLines        0
InternetService      0
OnlineSecurity       0
OnlineBackup         0
DeviceProtection     0
TechSupport          0
StreamingTV          0
StreamingMovies      0
Contract             0
PaperlessBilling     0
PaymentMethod        0
MonthlyCharges       0
TotalCharges        11
Churn                0
dtype: int64
In [31]:
# como há poucos dados ausentes (11) no dataframe, remove-se essas linhas utilizando o método dropna()

dados = dados.dropna()
In [32]:
# verificando novamente se há dados ausentes 

dados.isnull().sum()
Out[32]:
customerID          0
gender              0
SeniorCitizen       0
Partner             0
Dependents          0
tenure              0
PhoneService        0
MultipleLines       0
InternetService     0
OnlineSecurity      0
OnlineBackup        0
DeviceProtection    0
TechSupport         0
StreamingTV         0
StreamingMovies     0
Contract            0
PaperlessBilling    0
PaymentMethod       0
MonthlyCharges      0
TotalCharges        0
Churn               0
dtype: int64
In [33]:
# Utilizando o método Normalizer para normalização dos dados que estão em escalas diferentes de 0-1

from sklearn.preprocessing import Normalizer

dados_normal = dados[['tenure', 'MonthlyCharges' , 'TotalCharges']]
transformer = Normalizer().fit(dados_normal)
c = transformer.transform(dados_normal)
c = pd.DataFrame(c)
In [34]:
# concatenando o dataframe 'c' com o dataframe 'dados'

dados = pd.concat([dados, c], axis=1)
dados = dados.drop(columns=['tenure',	'MonthlyCharges' , 'TotalCharges'])
In [35]:
dados.head(0)
Out[35]:
customerID gender SeniorCitizen Partner Dependents PhoneService MultipleLines InternetService OnlineSecurity OnlineBackup ... TechSupport StreamingTV StreamingMovies Contract PaperlessBilling PaymentMethod Churn 0 1 2

0 rows × 21 columns

In [36]:
#renomeando as colunas 0, 1 e 2
dados.rename(columns={0: "tenure", 1: "MonthlyCharges", 2: 'TotalCharges'}, inplace=True)
In [37]:
# removendo a coluna 'customerID' por ser irrelevante para o treinamento do modelo

dados = dados.drop(columns=['customerID'])
In [38]:
dados.columns
Out[38]:
Index(['gender', 'SeniorCitizen', 'Partner', 'Dependents', 'PhoneService',
       'MultipleLines', 'InternetService', 'OnlineSecurity', 'OnlineBackup',
       'DeviceProtection', 'TechSupport', 'StreamingTV', 'StreamingMovies',
       'Contract', 'PaperlessBilling', 'PaymentMethod', 'Churn', 'tenure',
       'MonthlyCharges', 'TotalCharges'],
      dtype='object')
In [39]:
# ajustando ordem das colunas para melhor visualização

dados = dados[['gender', 'SeniorCitizen', 'Partner', 'Dependents', 'PhoneService',
       'MultipleLines', 'InternetService', 'OnlineSecurity', 'OnlineBackup',
       'DeviceProtection', 'TechSupport', 'StreamingTV', 'StreamingMovies',
       'Contract', 'PaperlessBilling', 'PaymentMethod', 'tenure',
       'MonthlyCharges', 'TotalCharges', 'Churn']]
In [40]:
# Dataframe final

dados.head(5)
Out[40]:
gender SeniorCitizen Partner Dependents PhoneService MultipleLines InternetService OnlineSecurity OnlineBackup DeviceProtection TechSupport StreamingTV StreamingMovies Contract PaperlessBilling PaymentMethod tenure MonthlyCharges TotalCharges Churn
0 1.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 1.0 0.0 0.023682 0.706908 0.706908 0.0
1 0.0 0.0 0.0 0.0 1.0 1.0 1.0 1.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.017983 0.030122 0.999384 0.0
2 0.0 0.0 0.0 0.0 1.0 1.0 1.0 1.0 1.0 0.0 0.0 0.0 0.0 1.0 1.0 0.0 0.016552 0.445662 0.895048 1.0
3 0.0 0.0 0.0 0.0 0.0 0.0 1.0 1.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 1.0 0.024433 0.022967 0.999438 0.0
4 1.0 0.0 0.0 0.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 1.0 0.0 0.011952 0.422512 0.906279 1.0
In [41]:
# plotando histogramas

dados.hist(figsize=(20,16))
plt.show()
  • Com esses histogramas percebe-se que há alguns dados desbalanceados.
In [42]:
# plotando gráfico de correlação entre os dados

plot_corr(dados)
In [43]:
# listagem do índice de correlação das colunas com a coluna 'Churn' por ordem decrescente

dados.corr().abs()['Churn'].sort_values(ascending = False)
Out[43]:
Churn               1.000000
Contract            0.404565
PaperlessBilling    0.191454
OnlineSecurity      0.171270
TechSupport         0.164716
Dependents          0.163128
SeniorCitizen       0.150541
Partner             0.149982
InternetService     0.124141
PaymentMethod       0.118136
OnlineBackup        0.082307
DeviceProtection    0.066193
StreamingTV         0.063254
StreamingMovies     0.060860
MonthlyCharges      0.036915
MultipleLines       0.032654
TotalCharges        0.031595
tenure              0.013636
PhoneService        0.011691
gender              0.008545
Name: Churn, dtype: float64
In [44]:
dados = dados.dropna()

6. Realizando predição com modelo de Machine Learning

In [45]:
# separando a variável alvo ('Churn') do dataframe

X = dados.drop(columns='Churn')

y = dados['Churn']
In [46]:
# utilizando o método train_test_split para realizar a separação do dados em treino e teste. Com proporção de 70% para treino 30% para teste

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state = 40)

Utilizaremos o método de validação cruzada com KFold para selecionar, comparativamente, o melhor modelo de machine learning.

In [59]:
# criando uma função para comparar os modelos de classificação utilizando o método de validação cruzada com KFold 

from sklearn.model_selection import KFold
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn import svm
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn import linear_model
from sklearn.linear_model import SGDClassifier
from sklearn.ensemble import AdaBoostClassifier
from sklearn.ensemble import GradientBoostingClassifier
import xgboost as xgb
from sklearn.metrics import accuracy_score


def ApplyesKFold(X, y):

  kfold  = KFold(n_splits=10, shuffle=True) # shuffle=True, Shuffle (embaralhar) os dados.

  # Models instances.
  Logistic_Regression = LogisticRegression(max_iter=500)
  KNN                 = KNeighborsClassifier()
  DTree               = DecisionTreeClassifier()
  Gaussian            = GaussianNB()
  SVM                 = svm.SVC()
  SGD                 = SGDClassifier(loss="hinge", penalty="l2", max_iter=1000)
  RFC                 = RandomForestClassifier(n_estimators=1000)
  AdaBoost            = AdaBoostClassifier(n_estimators=100, random_state=0)
  GradientBoosting    = GradientBoostingClassifier(n_estimators=100, learning_rate=1.0, max_depth=1, random_state=0)
  XGBClassifier       = xgb.XGBClassifier()
  

  # Aplica KFold aos modelos.
  Logistic_Regression_result  = cross_val_score(Logistic_Regression, X, y, cv = kfold, scoring='recall')
  KNN_result                  = cross_val_score(KNN, X, y, cv = kfold, scoring='recall')
  DTree_result                = cross_val_score(DTree, X, y, cv = kfold, scoring='recall')
  Gaussian_result             = cross_val_score(Gaussian, X, y, cv = kfold, scoring='recall')
  SVM_result                  = cross_val_score(SVM, X, y, cv = kfold, scoring='recall')
  SGD_result                  = cross_val_score(SGD, X, y, cv = kfold, scoring='recall')
  RFC_result                  = cross_val_score(RFC, X, y, cv = kfold, scoring='recall')
  AdaBoost_result             = cross_val_score(AdaBoost, X, y, cv = kfold, scoring='recall')
  GradientBoosting_result     = cross_val_score(GradientBoosting, X, y, cv = kfold, scoring='recall')
  XGBClassifier_result        = cross_val_score(XGBClassifier, X, y, cv = kfold, scoring='recall')

  # Cria um dicionário para gravar o resultado dos modelos
  dic_models = {
    "Logistic_Regression": Logistic_Regression_result.mean(),
    "KNN": KNN_result.mean(),
    "DTree": DTree_result.mean(),
    "Gaussian": Gaussian_result.mean(),
    "SVM": SVM_result.mean(),
    "SGD": SGD_result.mean(),
    "RFC": RFC_result.mean(),
    "AdaBoost": AdaBoost_result.mean(),
    "GradientBoosting": GradientBoosting_result.mean(),
    "XGBClassifier": XGBClassifier_result.mean()
  }


  # Seleciona o melhor modelo
  bestModel = max(dic_models, key=dic_models.get)
  porc = round(dic_models[bestModel]*100, 4)

  print(f"Logistic_Regression Mean (R^2): {Logistic_Regression_result.mean()} \nKNN Mean (R^2): {KNN_result.mean()} \nDTree Mean (R^2): {DTree_result.mean()} \nGaussian Mean (R^2): {Gaussian_result.mean()} \nSVM_result Mean (R^2): {SVM_result.mean()}") 
  print(f"SGD_result Mean (R^2): {SGD_result.mean()} \nRFC_result Mean (R^2): {RFC_result.mean()} \nAdaBoost_result Mean (R^2): {AdaBoost_result.mean()} \nGradientBoosting Mean(R^2): {GradientBoosting_result.mean()} \nXGBClassifier Mean (R^2): {XGBClassifier_result.mean()}")
 
  print("----------------------------------------------------------")
  print(f"O melhor modelo é {bestModel}, com recall: {porc} %")
In [60]:
# executando a função de comparação de modelos

ApplyesKFold(X_train, y_train)
[18:02:22] WARNING: C:/Users/Administrator/workspace/xgboost-win64_release_1.4.0/src/learner.cc:1095: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior.
[18:02:24] WARNING: C:/Users/Administrator/workspace/xgboost-win64_release_1.4.0/src/learner.cc:1095: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior.
[18:02:25] WARNING: C:/Users/Administrator/workspace/xgboost-win64_release_1.4.0/src/learner.cc:1095: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior.
[18:02:26] WARNING: C:/Users/Administrator/workspace/xgboost-win64_release_1.4.0/src/learner.cc:1095: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior.
[18:02:27] WARNING: C:/Users/Administrator/workspace/xgboost-win64_release_1.4.0/src/learner.cc:1095: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior.
[18:02:29] WARNING: C:/Users/Administrator/workspace/xgboost-win64_release_1.4.0/src/learner.cc:1095: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior.
[18:02:31] WARNING: C:/Users/Administrator/workspace/xgboost-win64_release_1.4.0/src/learner.cc:1095: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior.
[18:02:33] WARNING: C:/Users/Administrator/workspace/xgboost-win64_release_1.4.0/src/learner.cc:1095: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior.
[18:02:35] WARNING: C:/Users/Administrator/workspace/xgboost-win64_release_1.4.0/src/learner.cc:1095: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior.
[18:02:36] WARNING: C:/Users/Administrator/workspace/xgboost-win64_release_1.4.0/src/learner.cc:1095: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior.
Logistic_Regression Mean (R^2): 0.40684958525616455 
KNN Mean (R^2): 0.44743462605934425 
DTree Mean (R^2): 0.44106271888678944 
Gaussian Mean (R^2): 0.634323310169463 
SVM_result Mean (R^2): 0.3786866659964192
SGD_result Mean (R^2): 0.3563440032001766 
RFC_result Mean (R^2): 0.3790153596550467 
AdaBoost_result Mean (R^2): 0.3940253356227222 
GradientBoosting Mean(R^2): 0.40558050919122246 
XGBClassifier Mean (R^2): 0.39967986976661735
----------------------------------------------------------
O melhor modelo é Gaussian, com recall: 63.4323 %

O modelo com maior recall para esses dados é o Gaussian Naive Bayes.
Utilizaremos esse modelo para se ajustar aos dados de treino e realizar as predições nos dados de teste.

In [61]:
# ajuste do modelo aos dados de treino

gaussian = GaussianNB()

gaussian.fit(X_train, y_train)
print(f'O score do modelo treinado é: {round((gaussian.score(X_train, y_train)*100),2)} %')
O score do modelo treinado é: 73.79 %
In [62]:
# realizando predição utilizando os dados de teste

y_pred = gaussian.predict(X_test)

6.1 Avaliação das classificações

Para realizar a avaliação das classificações feitas pelo modelo, utilizaremos a matriz de confusão. Essa matriz mostra as frequências de classificação para cada classe do modelo.

Onde:

  • Verdadeiro positivo (true positive — TP): ocorre quando no conjunto de teste, a classe que estamos buscando foi prevista corretamente.

  • Falso positivo (false positive — FP): ocorre quando no conjunto de teste, a classe que estamos buscando prever foi prevista incorretamente.

  • Falso verdadeiro (true negative — TN): ocorre quando no conjunto de teste, a classe que não estamos buscando prever foi prevista corretamente.

  • Falso negativo (false negative — FN): ocorre quando no conjunto de teste, a classe que não estamos buscando prever foi prevista incorretamente.

In [52]:
# plotando a matriz de confusão

skplt.metrics.plot_confusion_matrix(y_test, y_pred, normalize=True)
Out[52]:
<AxesSubplot:title={'center':'Normalized Confusion Matrix'}, xlabel='Predicted label', ylabel='True label'>

Nesta matriz de confusão, verifa-se que 77% dos exemplos que são da classe 0 foram preditas corretamente. Assim como, 66% dos exemplos da classe 1 foram classificados corretamente.

Para demonstrar o desempenho do modelo podemos plotar a curva ROC (Receiver Operating Characteristic) por meio da relação da Taxa de Verdadeiro Positivo (Sensibilidade) e da Taxa de Falso Positivo, variando o threshold (ponto de corte na probabilidade estimada).

fonte: OpenEye Scientific

In [53]:
# plot do gráfico roc

metrics.plot_roc_curve(gaussian, X_test, y_test)  
plt.show()

Da matriz de confusão, podemos verificar algumas métricas de classificação do modelo nos dados.

In [54]:
# Obtendo métricas de classificação
# Sem desbalaceamento dos dados 
# Utilizando GaussianNB

from sklearn.metrics import classification_report
print(classification_report(y_test, y_pred, output_dict=False))
              precision    recall  f1-score   support

         0.0       0.86      0.77      0.81      1547
         1.0       0.50      0.66      0.57       560

    accuracy                           0.74      2107
   macro avg       0.68      0.71      0.69      2107
weighted avg       0.77      0.74      0.75      2107

Plotando o gráfico de contagem da variável alvo, percebe-se que há um desbalanceamento dos dados.
Por questão de comparação, foi implementado duas técnicas de over-sampling para desbalanceamento nos dados de treino (SMOTE e ADASYN), e realizou-se predição com modelos selecionados através da técnica de cross-validation.

In [55]:
# plotando gráfico de contagem da variável alvo

sns.countplot(y)
Out[55]:
<AxesSubplot:xlabel='Churn', ylabel='count'>

O resultado dos testes é apresentado abaixo.

Métricas de classificação com método SMOTE para desbalancemanto dos dados

# SMOTE
# Utilizando modelo SVM

              precision    recall  f1-score   support

         0.0       0.83      0.82      0.83      1547
         1.0       0.52      0.52      0.52       560

    accuracy                           0.74      2107
   macro avg       0.67      0.67      0.67      2107
weighted avg       0.74      0.74      0.74      2107

Métricas de classificação com método ADASYN para desbalancemanto dos dados

#ADASYN
#Utilizando modelo Random Forest

              precision    recall  f1-score   support

         0.0       0.82      0.83      0.83      1547
         1.0       0.52      0.49      0.51       560

    accuracy                           0.74      2107
   macro avg       0.67      0.66      0.67      2107
weighted avg       0.74      0.74      0.74      2107

Comparando os resultados, verifica-se que os valores de recall e f1-score dos modelos utilizados com as técnicas de desbalanceamento de dados, são piores na classe 1 do que o modelo utilizado com os dados desbalanceados.
Entretanto, o recall da classe 0 do modelo GaussianNB utilizado com os dados desbalanceados é razoavelmente pior do que os outros dois modelos testados.
Apesar disso, o f1-score do modelo treinado com dados desbalanceados é melhor na classe 1, além de ter a mesma acurácia dos outros modelos. Por isso, definiu-se o Gaussian Naive Bayes como modelo final.

In [56]:
import pickle
 
# salvar o modelo GaussianNB (gaussian) no arquivo churn_gaussian.pkl
with open('churn_gaussian.pkl', 'wb') as file:
    pickle.dump(gaussian, file)

7. Conclusão

Com este projeto foi possível verificar que os dados apresentados trazem algumas informações que podem traçar o perfil médio dos clientes que tendem a sair da empresa.
Clientes com mais serviços adicionais contratados e com mais tempo de contrato tendem a manter seu vínculo. Entretanto, clientes mais novos, maiores que 65 anos e com os maiores valores de planos, são propensos a deixar a empresa.
Por outro lado, os dados mostram que não há grandes disparidades entre os sexos dos clientes que deixaram a empresa, além de indicar que não há grande influência dos clientes que possuiam, ou não, linha telefônica, na decisão de deixar a empresa.
Por fim, foi implementado um modelo de machine learning com acurária de 74% para prever quais clientes que ainda tem vínculo com empresa são propensos em finalizar seu plano.